﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using log4net;

namespace SharePoint.Orm.Caml {
	public class LinqCamlRunner {
		private static readonly ILog _log = LogManager.GetLogger(typeof(LinqCamlRunner));

		public virtual IEnumerable Run<T>(IEnumerable<T> data, CamlQuery query) {
			data = RunWhere(data, query.Where);
			data = RunOrderBy(data, query.OrderBy);
			return RunGroupBy(data, query.GroupBy);
		}

		public virtual IEnumerable<T> RunWhere<T>(IEnumerable<T> data, CamlWhere where) {
			if (where == null || where.Root == null) {
				return data;
			}
			ParameterExpression itemParameter = Expression.Parameter(typeof(T), "item");
			Expression body = GetWhereExpression<T>(where.Root, itemParameter);
			LambdaExpression selector = Expression.Lambda(body, itemParameter);
			return RunMethod(data, "Where", false, selector, new[] { typeof(T) });
		}

		protected virtual Expression GetWhereExpression<T>(CamlExpr whereExpr, ParameterExpression itemParameter) {
			if (whereExpr == null) {
				throw new ArgumentNullException("whereExpr");
			}
			if (whereExpr is CamlLogicalOp) {
				return GetWhereExpression<T>((CamlLogicalOp)whereExpr, itemParameter);
			}
			if (whereExpr is CamlComparisonOp) {
				return GetWhereExpression<T>((CamlComparisonOp)whereExpr, itemParameter);
			}
			throw new Exception("Unexpected expression in where clause '" + whereExpr.GetType() + "'");
		}

		protected virtual Expression GetWhereExpression<T>(CamlLogicalOp whereExpr, ParameterExpression itemParameter) {
			Expression left = GetWhereExpression<T>(whereExpr.First, itemParameter);
			Expression right = GetWhereExpression<T>(whereExpr.Second, itemParameter);
			switch (whereExpr.Op) {
				case CamlLogicalOpType.And:
					return Expression.And(left, right);
				case CamlLogicalOpType.Or:
					return Expression.Or(left, right);
				default:
					throw new Exception("Unexpected logical operator '" + whereExpr.Op + "'");
			}
		}

		protected virtual Expression GetWhereExpression<T>(CamlComparisonOp whereExpr, ParameterExpression itemParameter) {
			Expression left = Expression.Property(itemParameter, whereExpr.FieldRef.FieldName);
			Expression right = GetConstantExpression(whereExpr.Value);
			switch (whereExpr.Op) {
				case CamlComparisonOpType.Equal:
					return Expression.Equal(left, right);
				default:
					throw new Exception("Unexpected comparison operator '" + whereExpr.Op + "'");
			}
		}

		protected virtual Expression GetConstantExpression(CamlValue camlValue) {
			switch (camlValue.Type) {
				case CamlValueType.String:
				case CamlValueType.Text:
					return Expression.Constant(camlValue.Value, typeof(string));
				case CamlValueType.Integer:
				case CamlValueType.Counter:
					return Expression.Constant(int.Parse(camlValue.Value), typeof(int));
				default:
					throw new Exception("Unexpected type '" + camlValue.Type + "'");
			}
		}

		public virtual IEnumerable<T> RunOrderBy<T>(IEnumerable<T> data, CamlOrderBy orderBy) {
			if (orderBy == null) {
				return data;
			}
			bool first = true;
			foreach (CamlFieldRef fieldRef in orderBy.Fields) {
				data = OrderBy(data, fieldRef, first);
				first = false;
			}
			return data;
		}

		protected virtual IEnumerable<T> OrderBy<T>(IEnumerable<T> data, CamlFieldRef fieldRef, bool first) {
			ParameterExpression itemParameter = Expression.Parameter(typeof(T), "item");
			Expression body = Expression.Property(itemParameter, fieldRef.FieldName);
			LambdaExpression keySelector = Expression.Lambda(body, itemParameter);
			string methodName;
			bool methodOnOrdered;
			if (fieldRef.IsAscending) {
				if (first) {
					methodName = "OrderBy";
					methodOnOrdered = false;
				} else {
					methodName = "ThenBy";
					methodOnOrdered = true;
				}
			} else {
				if (first) {
					methodName = "OrderByDescending";
					methodOnOrdered = false;
				} else {
					methodName = "ThenByDescending";
					methodOnOrdered = true;
				}
			}
			return RunMethod(data, methodName, methodOnOrdered, keySelector, new[] { typeof(T), GetFieldType<T>(fieldRef.FieldName) });
		}

		protected virtual IEnumerable<T> RunMethod<T>(IEnumerable<T> data, string methodName, bool methodOnOrdered, LambdaExpression selector, Type[] genericParameters) {
			Type sourceParameterType;
			Type keySelectorParameterType;
			Type methodSourceType;
			object parameter;
			if (data is IQueryable<T>) {
				// Expression<Func<T,TKey>>
				keySelectorParameterType = typeof(Expression<>).MakeGenericType(typeof(Func<,>));
				sourceParameterType = methodOnOrdered ? typeof(IOrderedQueryable<>) : typeof(IQueryable<>);
				parameter = selector;
				methodSourceType = typeof(Queryable);
			} else {
				// Func<T,TKey>
				keySelectorParameterType = typeof(Func<,>);
				sourceParameterType = methodOnOrdered ? typeof(IOrderedEnumerable<>) : typeof(IEnumerable<>);
				parameter = selector.Compile();
				methodSourceType = typeof(Enumerable);
			}
			MethodInfo method = FindGenericMethod(methodSourceType, methodName, new[] { sourceParameterType, keySelectorParameterType });
			if (method == null) {
				throw new Exception("Could not find method '" + methodName + "' on type '" + methodSourceType + "'");
			}
			method = method.MakeGenericMethod(genericParameters);
			return (IEnumerable<T>)method.Invoke(data, new[] { data, parameter });
		}

		private MethodInfo FindGenericMethod(Type type, string methodName, Type[] parameterTypes) {
			return type.GetMethods()
			 .FirstOrDefault(m => {
				 if (m.Name != methodName) {
					 return false;
				 }
				 ParameterInfo[] parameters = m.GetParameters();
				 if (parameters.Length != parameterTypes.Length) {
					 return false;
				 }
				 for (int i = 0; i < parameters.Length; i++) {
					 Type parameter = parameters[i].ParameterType;
					 Type expectedParameterType = parameterTypes[i];
					 if (expectedParameterType.Name != parameter.Name) {
						 return false;
					 }
				 }
				 return true;
			 });
		}

		public virtual IEnumerable RunGroupBy<T>(IEnumerable<T> data, CamlGroupBy groupBy) {
			// todo: write me
			return data;
		}

		protected virtual Type GetFieldType<T>(string fieldName) {
			PropertyInfo propertyInfo = typeof(T).GetProperty(fieldName);
			if (propertyInfo == null) {
				throw new Exception("could not find property with name '" + fieldName + "'");
			}
			return propertyInfo.PropertyType;
		}
	}
}
